﻿using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace EulerDB
{
    public class EulerGraph
    {
        Dictionary<Guid, Guid> IdLookup { get; set; } = new Dictionary<Guid, Guid>();

        public IList<EulerNode> Nodes { get; set; } = new List<EulerNode>();

        public IList<EulerEdge> Edges { get; set; } = new List<EulerEdge>();

        public EulerGraph()
        {

        }

        public EulerGraph(string filePath)
        {
            Load(filePath);
        }

        public void Save(string filePath)
        {
            var obj = JsonConvert.SerializeObject(this);
            File.WriteAllText(filePath, obj);
        }

        public void Load(string filePath)
        {
            try
            {
                var json = File.ReadAllText(filePath);
                var container = JsonConvert.DeserializeObject<EulerGraph>(json);

                Nodes = container.Nodes;
                Edges = container.Edges;
            }
            catch (Exception)
            {
                Nodes = new List<EulerNode>();
                Edges = new List<EulerEdge>();
            }
        }

        public void AddNode(string label, string nodeType, object bag)
        {
            Nodes.Add(new EulerNode { Id = Guid.NewGuid(), NodeLabel = label, NodeType = nodeType, Bag = GetPropertyBag(bag) });
        }

        public void DeleteNode(EulerNode node)
        {
            DeleteNode(node.Id);
        }

        public void DeleteNode(Guid node)
        {
            var existing = (from x in Nodes where x.Id == node select x).FirstOrDefault();

            if (existing == null)
                return;

            Nodes.Remove(existing);
        }

        public void DeleteEdge(EulerEdge edge)
        {
            DeleteEdge(edge.Id);
        }

        public void DeleteEdge(Guid edge)
        {
            var existing = (from x in Edges where x.Id == edge select x).FirstOrDefault();

            if (existing == null)
                return;

            Edges.Remove(existing);
        }

        static Dictionary<string, object> GetPropertyBag(object entity)
        {
            var bag = new Dictionary<string, object>();

            foreach (PropertyInfo property in entity.GetType().GetProperties())
            {
                var propValue = property.GetValue(entity, null);
                var name = property.Name;

                bag.Add(name, propValue);
            }

            return bag;
        }

        public void AddEdge(Guid incoming, Guid outgoing, string name)
        {
            var existing = (from x in Edges where x.IncomingId == incoming && x.OutgoingId == outgoing && string.Equals(name, x.Name, StringComparison.CurrentCultureIgnoreCase) select x).FirstOrDefault();
            if (existing != null)
                return;

            Edges.Add(new EulerEdge { Id = Guid.NewGuid(), IncomingId = incoming, OutgoingId = outgoing, Name = name });
        }

        public List<EulerNode> GetNodeIncomings(Guid node)
        {
            var incomingEdges = (from x in Edges where x.IncomingId == node select x.OutgoingId).ToList();

            var nodes = (from n in Nodes where (incomingEdges).Contains(n.Id) select n).ToList();

            return nodes;
        }

        public List<EulerNode> GetNodeOutgoings(Guid node)
        {
            var outgoingEdges = (from x in Edges where x.OutgoingId == node select x.IncomingId).ToList();

            var nodes = (from n in Nodes where (outgoingEdges).Contains(n.Id) select n).ToList();

            return nodes;
        }

        public List<EulerNode> GetNodeIncomings(EulerNode node)
        {
            return GetNodeIncomings(node.Id);
        }

        public List<EulerNode> GetNodeOutgoings(EulerNode node)
        {
            return GetNodeOutgoings(node.Id);
        }

        public List<EulerNode> GetNodeAssociations(EulerNode node)
        {
            return GetNodeAssociations(node.Id);
        }

        public List<EulerNode> GetNodeAssociations(Guid node)
        {
            var outgoings = GetNodeOutgoings(node);
            var incomings = GetNodeIncomings(node);

            outgoings.AddRange(incomings);

            return outgoings;
        }

        public void Fill<T>(IEnumerable<T> items, bool clear)
        {
            if (clear)
            {
                Nodes = new List<EulerNode>();
                Edges = new List<EulerEdge>();

                IdLookup = new Dictionary<Guid, Guid>();
            }

            CreateNodes(items);
            CreateEdges();
        }

        void CreateNodes<T>(IEnumerable<T> items)
        {
            foreach (var item in items)
            {
                ParseObject(item);
            }
        }

        void ParseObject(object item)
        {
            var node = new EulerNode
            {
                Id = Guid.NewGuid(),
                NodeLabel = item.GetType().Name,
                NodeType = item.GetType().Name,
                Bag = GetPropertyBag(item)
            };

            Nodes.Add(node);

            foreach (KeyValuePair<string, object> entry in node.Bag)
            {
                if (string.Equals(entry.Key, "Name", StringComparison.CurrentCultureIgnoreCase) &&
                    entry.Value is string)
                    node.NodeLabel = entry.Value.ToString();

                if (string.Equals(entry.Key, "Id", StringComparison.CurrentCultureIgnoreCase) &&
                    entry.Value is Guid)
                    IdLookup.Add(new Guid(entry.Value.ToString()), node.Id);
            }
        }

        void CreateEdges()
        {
            foreach (var node in Nodes)
            {
                var bagLinks = (from x in node.Bag
                                where x.Value is Guid && !string.Equals(x.Key, "Id", StringComparison.CurrentCultureIgnoreCase)
                                select x);

                foreach (var item in bagLinks)
                {
                    Guid lookupValue;
                    if (!IdLookup.TryGetValue((Guid)item.Value, out lookupValue))
                        continue;

                    var relation = (from x in Nodes where x.Id == lookupValue select x).FirstOrDefault();
                    if (relation == null || (node.Id == relation.Id))
                        continue;

                    AddEdge(relation.Id, node.Id, $"Connected to {relation.NodeLabel} - {node.NodeLabel}");
                }
            }
        }
    }
}
